# This program is licensed under the GNU GPL v2 (see [COPYING]) and
# [http://www.python.org/download/releases/2.4.2/license/ Python Licenses]

# Copyright Thomas Johler (c) 2007

import os, re, anydbm
import e32, appuifw, keycapture, thread
from   key_codes import *
try:
  import uitricks
  from   key_tricks import *
except:
  uitricks = None
try:
  import keypress
except:
  keypress = None

import dictflib, settings

APP_NAME  = u"PyDict"

DICT_PATH = [ r"E:\Dict", r"C:\Dict", r"E:\Others", r"C:\Data\Others" ]
LISTLEN   = 7
BUFFACT   = 8 # * LISTLEN
KEYPAD    = [ " ", "1", "A", "D", "G", "J", "M", "P", "T", "W" ]
uCRLF     = u'\u2029'
LINKPATT  = re.compile("{+([^\n]*?)}")
URLPATT   = re.compile(r'(http://[\w\d /.&+%~]*)')
HISTLEN   = 16

def setRlabel(text):
  if uitricks: uitricks.set_text(unicode(text), EAknSoftkeyExit)

def browse(url):
  brws = r'BrowserNG.exe'
  e32.start_exe(brws, ' "4 %s"' % url, 0)

class DictSettings(object):
  def __init__(self, raw=None):
    self.lastpos = 0
    self.search  = None
    self.open    = False
    self.history = []
    if raw:
      self.__dict__.update(eval(raw))

  def raw(self):
    return repr(self.__dict__)

class DictSettingsDB(object):
  def __init__(self, fname):
    self.db = anydbm.open(fname+".e32dbm", "cs")

  def __del__(self):
    self.close()

  def close(self):
    if self.db:
      self.db.reorganize()
      self.db.close()
      del self.db
      self.db = None

  def __getitem__(self, dict):
    raw = None
    if self.db.has_key(dict): raw = self.db[dict]
    return DictSettings(raw)

  def __setitem__(self, dict, settings):
    self.db[dict] = settings.raw()

class DictBuffer(object):
  def __init__(self, dict, pos, size):
    self.dict  = dict
    self.pos   = pos
    self.size  = size
    self.len   = size * BUFFACT
    self.start = self.abs(self.pos - self.size / 2 )
    
    if self.dict:
      self.load()
    else:
      self.buf = []
      self.buflen  = 0

  
  def abs(self, n):
    if self.dict == None: return 0
    return n % self.dict.numEntries

  def load(self):
    self.buf = []
    self.buflen  = 0
    for i in range(self.start, self.start+self.len):
      word, _def = self.dict.indexdef(self.abs(i))
      self.buf.append((unicode(word.decode("utf-8")),self.abs(i)))
      self.buflen += 1
      if self.buflen >= self.dict.numEntries: # tiny dictionary ;-)
        break

  def goto(self, pos):
    self.pos   = pos
    # if self.rel() < self.size or self.rel() >= self.len - self.size - 1:
    if self.rel() == 0 or self.rel() >= self.len-1:
      if self.rel() == 0: # browse backwards
        self.start = self.abs(self.pos-self.len+self.size+1)
      else:
        self.start = self.abs(self.pos-self.size)
      self.load()
      return True
    return False

  def move(self, n=1):
    return self.goto(self.abs(self.pos+n))
    
  def words(self):
    return [ b[0] for b in self.buf ]

  def rel(self):
    return self.abs(self.pos - self.start)

  def current(self, rel = None):
    if rel == None:
      rel = self.rel()
    return (self.buf[rel][0], self.buf[rel][1])

class DictionaryViewer(object):
  def run(self, standalone=False):
    appuifw.app.title       = APP_NAME
    appuifw.app.screen      = 'normal'
    appuifw.app.orientation = 'portrait'
    
    appuifw.app._self  = self
    if e32.in_emulator(): appuifw.app.body.clear()

    self.standalone = standalone
    self.lck  = e32.Ao_lock()
    
    self.listbox = appuifw.Listbox([ u"(No Dictionary loaded)" ], self.select)
    self.listbox.bind(EKeyUpArrow,    self.scrollUp)
    self.listbox.bind(EKeyDownArrow,  self.scrollDown)
    self.listbox.bind(EKeyIncVolume,  self.pageUp)
    self.listbox.bind(EKeyLeftArrow,  self.pageUp)
    self.listbox.bind(EKeyDecVolume,  self.pageDown)
    self.listbox.bind(EKeyRightArrow, self.pageDown)
    self.listbox.bind(EKeyYes,        self.selectHistory)
    startKey = EStdKeyF1
    startKey = EKey0
    numKeys  = 10
    for i in range(startKey, startKey+numKeys):
      if not keypress:
        self.listbox.bind(i, eval("lambda: appuifw.app._self.search(KEYPAD[%d-%d])" % (i, startKey) ))
      else:
        self.listbox.bind(i, eval("lambda: appuifw.app._self.search(%d)" % i ))
        
    self.text     = appuifw.Text()
    # TODO: font, size, ... from settings
    self.text.bind(EKeyYes,       self.textPageUp)
    self.text.bind(EKeyIncVolume, self.textPageUp)
    self.text.bind(EKeySelect,    self.textPageDown)
    self.text.bind(EKeyDecVolume, self.textPageDown)
    
    fonts = appuifw.available_fonts()
    desc  = [
      (("dictpath", "Dictionary Path"), "text",   [], unicode(";".join(DICT_PATH))),
      (("dictfile", "Last Dictionary"), "text",   [], u""),
      (("autocomp", "Compile Indices"), "combo",  [u"on", u"off"], u"on"),
      (("listlen",  "Visible Lines"),   "number", [], LISTLEN),
      (("histlen",  "History Size"),    "number", [], HISTLEN),
      (("viewfont", "Viewer Font"),     "combo",  fonts, self.text.font[0]),
      (("fontsize", "Font Size"),       "number", [], self.text.font[1]),
      (("color",    "Font Color"),      "text",   [], unicode(repr((0, 128, 0))) ),
    ]
    
    self.settings = settings.Settings(desc, "pydict.settings")
    
    self.applySettings()

    self.dictsdb  = DictSettingsDB(os.path.join(os.path.dirname(self.settings.fname),
                                                "pydict.dicts"))
    self.dictset  = None

    self.dict     = None
    self.idxsize  = 0
    self.timer    = None
    self.dictbuf  = None
    self.histClr  = False
    self.inViewer = False
    
    if not self.dictfile or not os.path.isfile(self.dictfile+".index"):
      self.openDict()
    else:
      self.loadDict()

    
    appuifw.app.body = self.listbox
    
    appuifw.app.exit_key_handler = self.close
    
    appuifw.app.menu = [
      (u'Search',            self.search),
      (u'View',              self.select),
      (u'History',           self.selectHistory),
      (u'Select Dictionary', self.openDict),
      (u'Index', (
        ( u'Compile',        self.compile),
        ( u'Delete compiled',self.delcomp),
        ( u'Help',           self.showIndexHelp),
      )),
      (u'Information',       self.showInfo),
      (u'Settings',          self.editSettings),
      (u'Help',              self.showHelp),
      (u'Exit',              self.close)
    ]
    
    self.lck.wait()
    
    if self.standalone:
      appuifw.app.set_exit()
    
    del self.listbox # otherwise: Panic CONE 8
    del self.text

    if self.timer:
      self.timer.cancel()
      del self.timer

    self.settings.save_settings()
    self.saveDictSettings()
    self.dictsdb.close()

    if self.dict:
      self.dict.close()

      
  def close(self):
    self.lck.signal()

    
  def openDict(self):
    avlbl = []
    # we'll have to do without 'glob'
    for pth in self.dictpath:
      if not os.path.isdir(pth):
        continue
      for f in os.listdir(pth):
        if not f.endswith(".index"):
          continue
        f = os.path.join(pth, f[:-6])
        if os.path.isfile(f + ".dict.dz") or os.path.isfile(f + ".dict"):
          avlbl += [ f ]
    avlbl.sort()
    choice = appuifw.selection_list([unicode(os.path.basename(a)) for a in avlbl], True)
    if choice == None:
      self.dictfile = self.settings.dictfile = u""
      return

    self.saveDictSettings()
    
    self.dictfile = avlbl[choice]
    self.loadDict()
    
  def editSettings(self):
    self.settings.execute_dialog()
    if self.settings.settings_changed:
      self.applySettings()
      
  def applySettings(self):
    self.listlen    = self.settings.listlen
    self.dictpath   = self.settings.dictpath.split(";")
    self.dictfile   = self.settings.dictfile
    self.autocomp   = (self.settings.autocomp == "on")
    self.text.font  = (unicode(self.settings.viewfont), self.settings.fontsize, 0x10)
    self.text.color = eval(self.settings.color)
    
  def saveDictSettings(self):
    if self.dictset:
      self.dictset.lastpos = self.dictbuf.pos
      self.dictsdb[self.dictfile] = self.dictset
      
  def search(self, word=""):
    if keypress and word:
      key = word
      def press():
        del self.timer
        self.timer = None
        keypress.simulate_key_mod(key, key, EModifierPureKeycode)
      self.timer = e32.Ao_timer()
      self.timer.after(0.1, press)
    if type(word) not in [unicode, str]:
      word = ""
    if not word and self.dictset.search:
      word = self.dictset.search
    word = appuifw.query(u"Find", "text", unicode(word))
    if not word:
      return
    self.dictset.search = word
    st, _len, idx = self.dict.findIndex(word.encode("utf-8"))
    self.dictbuf.goto(idx)
    self.reload()
    if st: # exact match
      self.select()
    
  def pushHist(self, word, idx):
    word = (word, idx)
    if word in self.dictset.history:
      self.dictset.history.remove(word)
    if len(self.dictset.history)+1 > self.settings.histlen:
      self.dictset.history.pop(-1)
    self.dictset.history.insert(0, word)

  def selectHistory(self):
    h = self.dictset.history[:]
    if h:
      h += [ (u'<Clear>',-2) ]
      if self.inViewer: 
        h += [ (u'Index',-1) ]
      sel = appuifw.popup_menu([e[0] for e in h], u'History')
      if sel == None:
        return False
      pos = h[sel][1]
      if pos == -2:
        self.dictset.history = []
        self.histClr = True
        if self.inViewer:
          setRlabel("Index")
        return False
      if pos >= 0:
        self.dictbuf.goto(pos)
        self.reload()
        if self.inViewer:
          self.delay(0.05, self.select)
      return True
    return False
    
  def addTxt(self, text):
    self.text.add(text.replace(u'\n', uCRLF))

  def setViewerText(self, text, pos):
    start = 0
    end   = len(text)
    self.text.clear()
    self.links = []
    color = self.text.color
    style = self.text.style
    error = ""
    for p in [LINKPATT, URLPATT]:
      for m in p.finditer(text):
        if len(self.links) < 128:
          self.addTxt(text[start:m.start(1)])
          self.text.color = (0, 0, 255); self.text.style |=  appuifw.STYLE_UNDERLINE
          self.addTxt(text[m.start(1):m.end(1)])
          self.text.color = color; self.text.style = style
          start = m.end(1)
        elif not error:
          error = u"Too many links, coloring suspended!\n"
        self.links += [ (m.start(1), m.end(1)) ]

    self.addTxt(text[start:end] + error)
    self.text.set_pos(pos)
    
    if error:
      e32.ao_yield()
      appuifw.note(error, "error")

  def viewText(self, word=None, text=None, pos=0, ishelp=False):
    if self.dict == None and self.text == None: return
    if word == None or text == None:
      word, idx = self.dictbuf.current()
      text = self.dict.indexdef(idx)[1]
      # if text.find('\n') != -1: # Seems like definition should be in the first line
      #   word, text = text.split("\n",1) # might be a accented encoding...
      text = unicode(text.decode("utf-8"))

    self.setViewerText(text, pos)

    self.viewlck = e32.Ao_lock()
    oldhdlr  = appuifw.app.exit_key_handler
    oldbody  = appuifw.app.body
    oldexit  = u"Exit" # :-(
    oldmenu  = appuifw.app.menu
    oldtitle = appuifw.app.title
    
    if ishelp:                     setRlabel("Back")
    elif not self.dictset.history: setRlabel("Index")
    else:                          setRlabel("History")

    self.dictset.open = False
    self.histCleared = False

    def exit():
      if not ishelp:
        self.dictset.open = True
      self.viewlck.signal()
      self.close()
    
    def back():
      closeme = True
      if self.dictset.history:
        closeme = self.selectHistory()
      if closeme:
        self.viewlck.signal()

    appuifw.app.menu = [
      (u'Index', self.viewlck.signal),
      (u'Help',  self.showViewerHelp),
      (u'Exit',  exit),
    ]
    appuifw.app.exit_key_handler = back
    appuifw.app.body  = self.text
    appuifw.app.title = unicode(word)

    if not ishelp:
      self.inViewer = True
    
    self.viewlck.wait()

    if not ishelp:
      if not self.histClr:
        self.pushHist(word, idx)
      else:
        self.histClr = False

      self.inViewer = False

    self.links = []

    setRlabel(oldexit)
    appuifw.app.exit_key_handler = oldhdlr
    appuifw.app.body  = oldbody
    appuifw.app.menu  = oldmenu
    appuifw.app.title = oldtitle


  def closeViewer(self):
    self.viewlck.signal()
    
  def select(self, word=None, pos=0):
    self.viewText(word=word, pos=pos)

  def __textlines(self):
    (_w, hw), (_x, _y) = appuifw.app.layout(appuifw.EMainPane)
    _n, hf, _f = self.text.font
    return int(hw/hf)
    
  def __moveLines(self, n):
    if self.dict == None: return
    if not keypress: 
      return
    key = ((n > 0) and EKeyDownArrow) or EKeyUpArrow
    n = abs(n)
    for i in range(n):
      keypress.simulate_key_mod(key,key, EModifierPureKeycode)
      e32.ao_sleep(0.01)

  def __delayed(self, call):
    del self.timer
    self.timer = None
    call()

  def delay(self, sec, call):
    self.timer = e32.Ao_timer()
    self.timer.after(sec, lambda: self.__delayed(call))

  def textPageUp(self):
    n = self.__textlines()
    if self.text.get_pos() == 0: return  self.closeViewer()
    self.__moveLines(-n)

  def textPageDown(self):
    p = self.text.get_pos()
    for s, e in self.links:
      if p >= s and p <= e:
        word = self.text.get()[s:e]
        if word[:5] == 'http:':
          browse(word)
          return
        st, _len, idx = self.dict.findIndex(word.encode("utf-8"))
        self.dictbuf.goto(idx)
        self.reload()
        self.closeViewer()
        e32.ao_yield()
        if st != None:
          self.delay(0.10, self.select)
        return
    t = self.text.get(); l = len(t); del t
    if p == l: return  self.closeViewer()
    n = self.__textlines()
    self.__moveLines(n)
    
  def reload(self):
    self.listbox.set_list(self.dictbuf.words(), self.dictbuf.rel())  
    
  def scrollUp(self):
    if self.dict == None or self.dictbuf.buflen == self.dict.numEntries: return
    reload = self.dictbuf.move(-1)
    if reload:
      self.delay(0.05, self.reload)
    
  def scrollDown(self):
    if self.dict == None or self.dictbuf.buflen == self.dict.numEntries: return
    reload = self.dictbuf.move(1)
    if reload:
      self.delay(0.05, self.reload)
      
  def pageUp(self):
    if self.dict == None or self.dictbuf.buflen == self.dict.numEntries: return
    self.dictbuf.move(-self.listlen)
    self.delay(0.05, self.reload)

  def pageDown(self):
    if self.dict == None or self.dictbuf.buflen == self.dict.numEntries: return
    self.dictbuf.move(self.listlen)
    self.delay(0.05, self.reload)
    
  def showInfo(self):
    if self.dict == None:
      info = [ u"No Dictionary loaded" ]
    else:
      info = [ u"%d Entries" % self.dict.numEntries,
               u"Index size %.2f MB" % self.idxsize ]
    appuifw.popup_menu(info, u"Information")

    
  def loadDict(self):
    self.idxsize = os.path.getsize(self.dictfile+".index")/1024.0/1024.0
    est = int(self.idxsize * 27.0 / 3.16)
    waitmsg = [u"(loading index...)" ] 
    if not os.path.isfile(self.dictfile+".cindex"):
      waitmsg += [ u"(%.2f MB, est. %d sec)" % (self.idxsize, est) ]
      if not self.autocomp:
        waitmsg += [ u"(Consider compiling", u"  the index)"]
      else:
        waitmsg += [ u"(compiling after", u"  loading)" ]
    self.listbox.set_list(waitmsg)
    appuifw.app.title = unicode(os.path.basename(self.dictfile).replace(".index",""))
    e32.ao_yield()
    thread.start_new_thread(e32.ao_callgate(self.__loadThread), () )

  def __loadThread(self):
    try:
      self.dict    = dictflib.DictDB(self.dictfile)
      self.settings.dictfile = unicode(self.dictfile)
      if self.autocomp:
        self.compile(overwrite=False)
      self.dictset  = self.dictsdb[self.dictfile]
      self.dictbuf  = DictBuffer(self.dict, self.dictset.lastpos, self.listlen)
      self.reload()
      if self.dictset.open:
        self.delay(0.1, self.select)

    except Exception, exc:
      appuifw.app.title = APP_NAME
      appuifw.note(unicode(exc), "error")
      self.listbox.set_list([u"(ERROR)"])
      self.settings.dictfile = u""

  def compile(self, overwrite=True):
    if not self.dict: return
    already = self.dict.iscompiled
    self.dict.compileIndex(overwrite)
    if not already:
      appuifw.note(u"Compiled index saved.", "conf")

  def delcomp(self):
    if not self.dict: return
    self.dict.deleteCompiled()

  def showHelp(self, help=None):
    if not help:
      help = self.HELP_TEXT_COMMON + self.HELP_TEXT_FULL
      # if not keypress: help += self.HELP_TEXT_MISSING
      # else:            help += self.HELP_TEXT_FULL
    self.viewText("Help", unicode(help).replace(u"\n", uCRLF), ishelp=True)
    
  def showViewerHelp(self):
    help = self.HELP_VIEWER_COMMON
    if not keypress: help += self.HELP_VIEWER_MISSING
    else:            help += self.HELP_VIEWER_FULL
    pos = self.text.get_pos()
    self.closeViewer()
    e32.ao_yield()
    self.showHelp(help)
    self.select(pos=pos)

  def showIndexHelp(self):
    self.showHelp(HELP_TEXT_INDEX)
    
  HELP_TEXT_COMMON = """To search, just start typing a letter or select \
'Search' from the menu. Dictionaries should be installed in the \
Path (see 'Settings') and are in the dictd format, available \
e.g. at freedict.org (compressed or uncompressed).

Please see also the help in the 'Index' menu and in this viewer.
"""

  HELP_TEXT_MISSING = """
If you want full functionality (page up/down, real search typing), install:

 - keypress and
 - uitricks
 
both can be found at http://cyke64.googlepages.com/

Unfortunately, the keypress module requires SwEvent capabilities, so you \
have to sign it, as well as PyDict, yourself (ensymble is a good choice).

If you don't know what we're ralking about now, it's probably easier, to \
leave things the way they are now. It's still usable without.
"""

  HELP_TEXT_FULL = """
To scroll the list a page down press the right arrow,
to scroll a page up, the left arrow.
"""

  HELP_VIEWER_COMMON = """Arrow keys scroll by line or select characters \
(using the 'edit' key)
"""
  
  HELP_VIEWER_MISSING = HELP_TEXT_MISSING

  HELP_VIEWER_FULL = """
Press 'Enter' (or 'Select') to go to the next page, 'Yes' (green key) to go"""\
"""to the previous page.
"""

HELP_TEXT_INDEX = """Indices must be sorted for the search algorithm to work.
This can be achieved by running the included 'sortidx.py' on your PC, this \
gives the best result with non-english characters.
If you don't have Python installed you may use the command

'sort <InputIndex> /L ""C"" /O <OutputIndex>'

under Windows. As the UN*X variants of 'sort' ignore UTF-8 encoding, it's \
probably easier to install Python and run the script...

By the default, textual indices are compiled into binary form and saved in the \
dictionary's directory. You can turn this setting off and delete indices for \
a loaded dictionary.

If you re-install a dictionary with a new index, you should also delete it's \
compiled index an re-compile.
"""

if __name__ in ['__main__', 'editor']:
  DictionaryViewer().run()
